package com.beans;


import java.beans.*;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class BeanUtils {
    private Map descriptorsCache = null;
    private Map mappedDescriptorsCache = null;
    private DefaultResolver resolver = new DefaultResolver();

    public BeanUtils() {
        descriptorsCache = new HashMap();
        mappedDescriptorsCache = new HashMap();
    }

    public static void main(String[] args) {
        ProductBean bean = new ProductBean();
        // bean.setSid(1L);
        bean.setAirlinesCode("airChinaCode");
        bean.setCreatedAt(new Date());

        Map map = getAllProperties(bean);


        Product product = new Product();
        product.setSid(100L);
        product.setNoteList(Arrays.asList("note1", "note2", "note3"));
        copySelectProperties(bean, product);
        new BeanUtils().commonProperties(product.getClass(),bean.getClass());
    }

    private static Map<String, Object> getAllProperties(Object bean) {
        Map<String, Object> map = new HashMap<>();
        if (bean == null) {
            return map;
        }
        Class clz = bean.getClass();
        try {
            while (clz != null) {
                Field[] fields = clz.getDeclaredFields();
                for (int i = 0; i < fields.length; i++) {
                    Field field = fields[i];
                    field.setAccessible(true);
                    map.put(field.getName(), field.get(bean));
                }
                clz = clz.getSuperclass();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return map;
    }

    private static void copyProperties(Object from, Object to) {
        copyProperties(from, to, false);
    }

    public static void copySelectProperties(Object from, Object to) {
        copyProperties(from, to, true);
    }

    private static void copyProperties(Object from, Object to, boolean isSelected) {
        Map<String, Object> fromMap = getAllProperties(from);
        Map<String, Object> toMap = getAllProperties(to);
        Class toClass = to.getClass();

        fromMap.forEach((key, val) -> {
            try {
                if (toMap.containsKey(key)) {
                    Field field = toClass.getDeclaredField(key);
                    field.setAccessible(true);
                    if (!isSelected) {
                        field.set(to, val);
                    } else if (val != null) {
                        field.set(to, val);
                    }
                }
            } catch (NoSuchFieldException | IllegalAccessException e) {
                e.printStackTrace();
            }
        });

        System.out.println(to);
    }

    public void commonProperties(Object dest, Object orig) {
        if (dest == null) {
            throw new IllegalArgumentException("No destination bean specified");
        }

        if (orig == null) {
            throw new IllegalArgumentException("No origin bean specified");
        }

        if (orig instanceof Map) {
            ((Map) orig).forEach((k, v) -> {
                PropertyDescriptor propertyDescriptor = getPropertyDescriptor(orig, (String) k);
                Method writeMethod = propertyDescriptor.getWriteMethod();
                if (writeMethod != null) {
                    try {
                        setSimpleProperty(dest, (String)k, v);
                    } catch (NoSuchMethodException e) {
                        e.printStackTrace();
                    }
                }
            });
        } else {
            PropertyDescriptor[] origDescriptors = getPropertyDescriptors(orig.getClass());
            for (PropertyDescriptor descriptor:origDescriptors) {
                String name = descriptor.getName() ;
                if(isReadable(orig,name) && isWriteable(dest,name)){
                    Object value = null;
                    try {
                        value = getSimpleProperty(orig, name);
                        setSimpleProperty(dest,name,value);
                    } catch (NoSuchMethodException e) {
                        e.printStackTrace();
                    }

                }
            }
        }
    }

    public PropertyDescriptor[] getPropertyDescriptors(Class bean) {
        PropertyDescriptor[] beanDescriptors = null;
        if (bean == null) {
            throw new IllegalArgumentException("No bean class specified");
        }
        BeanInfo beanInfo = null;
        try {
            beanInfo = Introspector.getBeanInfo(bean);
        } catch (IntrospectionException e) {
            e.printStackTrace();
            //如果出现异常，返回一个空的数组。
            return new PropertyDescriptor[0];
        }
        beanDescriptors = beanInfo.getPropertyDescriptors();
        if (beanDescriptors == null) {
            beanDescriptors = new PropertyDescriptor[0];
        }

        for (PropertyDescriptor it : beanDescriptors) {
            if (it instanceof IndexedPropertyDescriptor) {
                throw new RuntimeException("java 版本过低");
            }
        }
        descriptorsCache.put(bean, beanDescriptors);
        return beanDescriptors;
    }

    public PropertyDescriptor[] getPropertyDescriptors(Object bean) {
        if (bean == null) {
            throw new IllegalArgumentException("bean is empty");
        }
        return getPropertyDescriptors(bean.getClass());
    }

    public Class getPropertyEditorClass(Object bean, String name) {
        if (bean == null) {
            throw new IllegalArgumentException("bean is empty");
        }
        if (name == null || name == "") {
            throw new IllegalArgumentException("name is empty for bean class");
        }

        PropertyDescriptor descriptor = getPropertyDescriptor(bean, name);

        if (descriptor != null) {
            return (descriptor.getPropertyEditorClass());
        }

        return null;
    }

    public PropertyDescriptor getPropertyDescriptor(Object bean, String name) {
        if (bean == null) {
            throw new IllegalArgumentException("bean is empty");
        }

        if (name == null || name == "") {
            throw new IllegalArgumentException("name is empty for bean class");
        }

        while (resolver.hasNested(name)) {
            String next = resolver.next(name);
            Object nestedBean = getProperty(bean, next);
            if (nestedBean == null) {
                throw new IllegalArgumentException("Null property value for '" + next +
                        "' on bean class '" + bean.getClass() + "'");
            }
            bean = nestedBean;
            name = resolver.remove(name);
        }
        name = resolver.getProperty(name);
        if (bean == null || name == null) {
            return null;
        }

        PropertyDescriptor[] descriptors = getPropertyDescriptors(bean);
        if (descriptors != null) {
            for (PropertyDescriptor descriptor : descriptors) {
                if (name.equals(descriptor.getName())) {
                    return descriptor;
                }
            }
        }

        PropertyDescriptor result = null;
        Map mappedDescriptors = getMappedPropertyDescriptors(bean.getClass());
        if (mappedDescriptors == null) {
            mappedDescriptors = new HashMap();
            mappedDescriptorsCache.put(bean.getClass(), mappedDescriptors);
        }
        result = (PropertyDescriptor) mappedDescriptors.get(name);
        if (result == null) {
            try {
                result = new MappedPropertyDescriptor(name, bean.getClass());
            } catch (IntrospectionException ie) {

            }
            if (result != null) {
                mappedDescriptors.put(name, result);
            }
        }
        return result;
    }

    public Object getProperty(Object bean, String name) {
        return getNestedProperty(bean, name);
    }

    public Object getNestedProperty(Object bean, String name) {
        if (bean == null) {
            throw new IllegalArgumentException("bean is empty");
        }

        if (name == null || name == "") {
            throw new IllegalArgumentException("name is empty for bean class");
        }

        while (resolver.hasNested(name)) {
            String next = resolver.next(name);
            Object nestedBean = null;
            if (bean instanceof Map) {
                nestedBean = getPropertyOfMapBean((Map) bean, next);
            }

            if (nestedBean == null) {
                throw new IllegalArgumentException("Null property value for '" +
                        name + "' on bean class '" + bean.getClass() + "'");
            }
            bean = nestedBean;
            name = resolver.remove(name);
        }

        if (bean instanceof Map) {
            bean = getPropertyOfMapBean((Map) bean, name);
        }
        return bean;
    }

    protected Object getPropertyOfMapBean(Map bean, String propertyName) {
        if (resolver.isMapped(propertyName)) {
            String name = resolver.getProperty(propertyName);
            if (name == null || name.length() == 0) {
                propertyName = resolver.getKey(propertyName);
            }
        }

        if (resolver.isIndexed(propertyName) ||
                resolver.isMapped(propertyName)) {
            throw new IllegalArgumentException("List or mapp are not supported on " +
                    " objects of type mpa: " + propertyName);
        }

        return bean.get(propertyName);
    }

    public Map getMappedPropertyDescriptors(Class beanClass) {
        if (beanClass == null) {
            return null;
        }

        return (Map) mappedDescriptorsCache.get(beanClass);
    }

    public void setSimpleProperty(Object bean, String name, Object value) throws NoSuchMethodException {
        if (bean == null) {
            throw new IllegalArgumentException("No bean specified");
        }

        if (name == null || name.length() == 0) {
            throw new IllegalArgumentException("No name specified for bean class '" + bean.getClass() + "'");
        }

        if (resolver.hasNested(name)) {
            throw new IllegalArgumentException("Nested property names are not allowed:Property'" +
                    name + "' on bean class '" + bean.getClass() + ".'");
        } else if (resolver.isIndexed(name)) {
            throw new IllegalArgumentException("Indexed property names are not allowed:Property'" +
                    name + "' on bean class '" + bean.getClass() + ".'");
        } else if (resolver.isMapped(name)) {
            throw new IllegalArgumentException
                    ("Mapped property names are not allowed: Property '" +
                            name + "' on bean class '" + bean.getClass() + "'");
        }

        PropertyDescriptor descriptor = getPropertyDescriptor(bean, name);
        if (descriptor == null) {
            throw new NoSuchMethodException("Unknown property '" +
                    name + "' on class '" + bean.getClass() + "'");
        }

        Method writedMethod = getWriteMethod(bean.getClass(), descriptor);
        if (writedMethod == null) {
            throw new NoSuchMethodException("Property '" + name +
                    "' has no setter method in class '" + bean.getClass() + "'");
        }

        Object[] values = new Object[1];
        values[0] = value;
        try {
            writedMethod.invoke(bean, values);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }

    private Method getWriteMethod(Class clz, PropertyDescriptor descriptor) {
        Method writeMethod = descriptor.getWriteMethod();
        return MethodUtils.getAccessibleMethod(clz, writeMethod);
    }

    private Method getReadMethod(Class clz, PropertyDescriptor descriptor) {
        Method readMethod = descriptor.getReadMethod();
        return MethodUtils.getAccessibleMethod(clz, readMethod);
    }

    public boolean isReadable(Object bean,String name){
        if(bean == null){
            throw new IllegalArgumentException("No bean specified");
        }

        if(name == null || name.length() == 0){
            throw new IllegalArgumentException("No name specified for bean class '" +
                    bean.getClass() + "'");
        }

        while(resolver.hasNested(name)){
            String next = resolver.next(name) ;
            Object nestedBean = null ;
            try{
                nestedBean = getProperty(bean,next) ;
            }catch (Exception e){
                return false ;
            }

            if(nestedBean == null){
                throw new IllegalArgumentException
                        ("Null property value for '" + next +
                                "' on bean class '" + bean.getClass() + "'");
            }
            bean = nestedBean ;
            name = resolver.remove(name) ;
        }

        name = resolver.getProperty(name) ;

        PropertyDescriptor desc =
                getPropertyDescriptor(bean,name) ;

        if(desc!= null){
            Method readMethod = getReadMethod(bean.getClass(), desc);
            if(readMethod == null){
                if(desc instanceof IndexedPropertyDescriptor){
                    readMethod = ((IndexedPropertyDescriptor) desc).getIndexedReadMethod() ;
                }else if (desc instanceof MappedPropertyDescriptor){
                    readMethod = ((MappedPropertyDescriptor) desc).getMappedReadMethod() ;
                }
                readMethod = MethodUtils.getAccessibleMethod(bean.getClass(),readMethod) ;
            }
            return readMethod != null ;
        }else {
            return  false ;
        }
    }

    public boolean isWriteable(Object bean,String name){
        if (bean == null) {
            throw new IllegalArgumentException("No bean specified");
        }
        if (name == null || name.length() == 0) {
            throw new IllegalArgumentException("No name specified for bean class '" +
                    bean.getClass() + "'");
        }

        while (resolver.hasNested(name)){
            String next = resolver.next(name) ;
            Object nestedBean = null ;

            nestedBean = getProperty(bean,next) ;

            if (nestedBean == null) {
                throw new IllegalArgumentException
                        ("Null property value for '" + next +
                                "' on bean class '" + bean.getClass() + "'");
            }
            bean = nestedBean;
            name = resolver.remove(name);
        }

        name = resolver.getProperty(name) ;
        PropertyDescriptor desc = getPropertyDescriptor(bean,name) ;
        if(desc != null){
            Method writeMethod = getWriteMethod(bean.getClass(),desc) ;
            if(writeMethod == null){
                if(desc instanceof IndexedPropertyDescriptor){
                    writeMethod = ((IndexedPropertyDescriptor) desc).getIndexedWriteMethod();
                }else if(desc instanceof MappedPropertyDescriptor){
                    writeMethod = ((MappedPropertyDescriptor) desc).getMappedWriteMethod() ;
                }
                writeMethod = MethodUtils.getAccessibleMethod(bean.getClass(),writeMethod) ;
            }
            return writeMethod != null ;
        }else {
            return  false ;
        }
    }

    public Object getSimpleProperty(Object bean,String name) throws NoSuchMethodException{
        if (bean == null) {
            throw new IllegalArgumentException("No bean specified");
        }
        if (name == null || name.length() == 0) {
            throw new IllegalArgumentException("No name specified for bean class '" +
                    bean.getClass() + "'");
        }

        if(resolver.hasNested(name)){
            throw new IllegalArgumentException
                    ("Nested property names are not allowed: Property '" +
                            name + "' on bean class '" + bean.getClass() + "'");
        }else if(resolver.isIndexed(name)){
            throw new IllegalArgumentException
                    ("Indexed property names are not allowed: Property '" +
                            name + "' on bean class '" + bean.getClass() + "'");
        }else if(resolver.isMapped(name)){
            throw new IllegalArgumentException
                    ("Mapped property names are not allowed: Property '" +
                            name + "' on bean class '" + bean.getClass() + "'");
        }

        PropertyDescriptor descriptor = getPropertyDescriptor(bean,name) ;
        if(descriptor == null){
            throw new NoSuchMethodException("Unknown property '" +
                    name + "' on class '" + bean.getClass() + "'" );
        }
        Method readMethod = getReadMethod(bean.getClass(),descriptor) ;
        if(readMethod == null){
            throw new NoSuchMethodException("Property '" + name +
                    "' has no getter method in class '" + bean.getClass() + "'");
        }
        Object value = null  ;
        try {
             value = readMethod.invoke(bean, new Object[0]) ;
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        return value ;
    }

}
